package com.eugenema.util;

import com.alibaba.excel.util.StringUtils;
import com.eugenema.entity.ParameterMap;
import com.eugenema.entity.TestCase;
import com.eugenema.entity.TestStep;
import com.eugenema.entity.TestUnit;
import com.eugenema.enumEntity.Locator;
import com.eugenema.enumEntity.StepAction;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Selenium 执行脚本 XML 解析工具类
 *
 * @author Eugenema
 * @date 2023/3/8 22:38
 */
public class SeleniumXmlParseUtil {
    private static Logger logger = LoggerFactory.getLogger(SeleniumXmlParseUtil.class);

    /**
     * 布尔值：true
     */
    public static final String BOOLEAN_TRUE = "true";
    /**
     * 公共参数列表，支持 map<String, String>、map<String, List<String>>、map<String, map<String, String>>
     */
    public static Map<String, Object> parameterMap = new LinkedHashMap<>(16);
    /** 修改脚本文件值的分隔符 */
    public static String SEPARATOR_SCRIPT_UPDATE = " separator ";
    /**
     * 错误提示信息开头：解析 Script 脚本失败：
     */
    private static final String ERROR_TIP_PARSE = "解析 Script 脚本失败：";
    /**
     * 错误提示信息开头：通过反射为对象属性赋值失败：
     */
    private static final String ERROR_TIP_REFLECT = "通过反射为对象属性赋值失败：";
    /**
     * 错误信息开头：对象属性值初始化失败：
     */
    private static final String ERROR_TIP_INIT_ATTR = "对象属性值初始化失败：";
    private static final String ERROR_TIP_PARAMETER_PARSE_ATTR = "解析指定 XML 的参数异常：";
    private static final String ERROR_TIP_WRITE_PARAMETER_ATTR = "参数写入 XML 异常：";

    /**
     * 特殊属性名赋值：action
     */
    private static final String SPECIAL_ACTION = "action";
    /**
     * 特殊属性名赋值：cancel
     */
    private static final String SPECIAL_CANCEL = "cancel";
    /**
     * 特殊属性名赋值：locator
     */
    private static final String SPECIAL_LOCATOR = "locator";
    /**
     * 特殊属性名赋值：noHeadLess
     */
    private static final String SPECIAL_NO_HEAD_LESS = "noHeadLess";
    /**
     * 特殊属性名赋值：profile
     */
    private static final String SPECIAL_PROFILE = "profile";
    /**
     * 特殊属性名赋值：excelEntity
     */
    private static final String SPECIAL_EXCEL_ENTITY = "excelEntity";
    /**
     * 特殊属性名赋值：readListener
     */
    private static final String SPECIAL_READ_LISTENER = "readListener";

    /**
     * 元素名：步骤 case
     */
    private static final String ELEMENT_CASE = "case";
    /**
     * 元素名：步骤 step
     */
    private static final String ELEMENT_STEP = "step";
    /**
     * 元素名：参数 parameter
     */
    private static final String ELEMENT_PARAMETER = "parameter";
    /**
     * 元素名：参数 parameters
     */
    private static final String ELEMENT_PARAMETERS = "parameters";
    /**
     * 元素名：List 元素 parameterItem
     */
    private static final String ELEMENT_PARAMETER_ITEM = "parameterItem";

    /**
     * 属性名：参数 key
     */
    private static final String ATTR_KEY = "key";
    /**
     * 属性名：参数 value
     */
    private static final String ATTR_VALUE = "value";

    /**
     * 方法前缀：get
     */
    private static final String METHOD_PRE_GET = "get";
    /**
     * 方法前缀：set
     */
    private static final String METHOD_PRE_SET = "set";

    /**
     * 要解析的 XML 路径：src/main/resources/parseXml
     */
    public static final String PARSE_XML_PATH = System.getProperty("user.dir") + File.separator + "resources" + File.separator + "parseXml" + File.separator;

    /**
     * EL 表达式格式校验正则表达式
     */
    private static final String REGEX_EL_FORMAT = "^\\$\\{[a-zA-Z0-9_]+(\\[[0-9_]+\\])?\\}$";
    /**
     * 去除 ${} 的正则表达式
     */
    private static final String REGEX_TRIM = "\\$|\\{|\\}";
    /**
     * 分割 key 和索引的正则表达式
     */
    private static final String REGEX_SPLIT = "(\\w+)\\[?(\\d+)?\\]?";

    /**
     * 读取 xml 文件，解析成对象
     *
     * @param xmlFileName 文件路径或 SeleniumXmlParseUtil.PARSE_XML_PATH 路径下的文件名
     * @return
     * @author Eugenema
     * @date 2023/10/21 23:12
     **/
    private static Document readXml(String xmlFileName) {
        logger.info("正在读取【{}】文件", xmlFileName);
        File xmlFile = getTransferFile(xmlFileName);

        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlFile);
        } catch (Exception e) {
            throw new RuntimeException(ERROR_TIP_PARSE, e);
        }
    }

    /**
     * 获取转换后的文件
     *
     * @param xmlFileName
     * @return 若是全路径，则返回 file 对象；若是文件名，则指向 SeleniumXmlParseUtil.PARSE_XML_PATH + 文件名路径，并返回 file 对象；
     * @author Eugenema
     * @date 2023/10/21 17:08
     **/
    private static File getTransferFile(String xmlFileName) {
        File xmlFile = new File(xmlFileName);
        if (!xmlFile.isFile()) {
            xmlFile = new File(PARSE_XML_PATH + xmlFileName);
        }
        if (!xmlFile.exists()) {
            throw new RuntimeException(String.format("%s Xml 文件【%s】为空", ERROR_TIP_PARSE, xmlFile.getAbsolutePath()));
        }
        return xmlFile;
    }

    /**
     * 解析 XML 文件
     * <br/>若有参数，则初始化公共参数列表 parameterMap
     *
     * @param xmlFileName Xml 文件名，包括扩展名
     * @return 对应的实体类
     * @author Eugenema
     * @date 2023/3/8 22:39
     **/
    public static TestUnit parse(String xmlFileName) {
        logger.info("正在解析【{}】文件", xmlFileName);

        TestUnit testUnit;
        try {
            Document doc = readXml(xmlFileName);
            Element root = doc.getDocumentElement();
            /** 要解析的子元素列表 */
            NodeList sonNodeList;

            //解析 unit 元素
            testUnit = initToAttributes(root.getAttributes(), new TestUnit());

            //解析 case 子元素
            sonNodeList = root.getElementsByTagName(ELEMENT_CASE);
            TestCase testCase;
            /** case 元素列表 */
            LinkedHashSet<TestCase> caseSet = new LinkedHashSet<>();
            for (int i = 0; i < sonNodeList.getLength(); i++) {
                testCase = parseTestCase((Element) sonNodeList.item(i));

                //校验 case 元素 id
                if (!caseSet.add(testCase)) {
                    logger.error("{}case 元素 id 属性存在多个【{}】，请检查 id 配置", ERROR_TIP_PARSE, testCase.getId());
                    throw new RuntimeException(String.format("%scase 元素 id 属性存在多个【%s】，请检查 id 配置", ERROR_TIP_PARSE, testCase.getId()));
                }
            }
            testUnit.setCaseSet(caseSet);
        } catch (Exception e) {
            throw new RuntimeException(ERROR_TIP_PARSE, e);
        }
        return testUnit;
    }

    /**
     * 解析指定 XML 的参数
     *
     * @param xmlFileName 要解析的 XML
     * @author Eugenema
     * @date 2023/5/5 21:02
     **/
    public static void parseParameter(String xmlFileName) {
        try {
            Document doc = readXml(xmlFileName);
            Element root = doc.getDocumentElement();

            //解析 parameter 子元素
            /** 要解析的子元素列表 */
            NodeList sonNodeList = root.getElementsByTagName(ELEMENT_PARAMETER);
            if (sonNodeList.getLength() > 0) {
                for (int i = 0; i < sonNodeList.getLength(); i++) {
                    parseParameter((Element) sonNodeList.item(i));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(ERROR_TIP_PARAMETER_PARSE_ATTR, e);
        }
    }

    /**
     * 将修改后的参数写入 XML
     *
     * @param modifyParameter 要写入的参数
     * @author 马寅广
     * @date 2023/5/6 17:44
     **/
    public static void writeParameter(String xmlFileName, ObservableList<ParameterMap> modifyParameter) {
        try {
            Document doc = readXml(xmlFileName);
            Element root = doc.getDocumentElement();

            /** 要修改的参数 key */
            String parameterKey = modifyParameter.get(0).parameterKey().get();

            //解析 parameter 子元素
            /** 解析子元素的父元素 */
            Element parameters = (Element) root.getElementsByTagName(ELEMENT_PARAMETERS).item(0);
            /** 要解析的子元素列表 */
            NodeList parameterNodeList = parameters.getElementsByTagName(ELEMENT_PARAMETER);
            /** 参数子元素 */
            Element parameter;
            if (parameterNodeList.getLength() > 0) {
                for (int i = 0; i < parameterNodeList.getLength(); i++) {
                    parameter = (Element) parameterNodeList.item(i);
                    //查找要修改的 parameter
                    if (parameter.getAttribute(ATTR_KEY).equals(parameterKey)) {
                        //删除 parameter
                        Node node = parameters.removeChild(parameter);
                        //新增 parameter
                        parameter = doc.createElement(ELEMENT_PARAMETER);
                        parameter.setAttribute(ATTR_KEY, parameterKey);
                        //插入新参数项
                        for (ParameterMap parameterMap : modifyParameter) {
                            Element addParameterItem = doc.createElement(ELEMENT_PARAMETER_ITEM);
                            addParameterItem.setAttribute(ATTR_KEY, parameterMap.nameProperty().get());
                            addParameterItem.setAttribute(ATTR_VALUE, parameterMap.linkProperty().get());
                            parameter.appendChild(addParameterItem);
                        }
                        //插入到文件中
                        parameters.appendChild(parameter);
                        break;
                    }
                }
            }

            writerXmlFile(doc, getTransferFile(xmlFileName));
        } catch (Exception e) {
            throw new RuntimeException(ERROR_TIP_WRITE_PARAMETER_ATTR, e);
        }
    }

    /**
     * 写入 xml 文件
     *
     * @param doc     要写入的内容
     * @param xmlFile 要写入的 xml 文件
     * @author Eugenema
     * @date 2023/10/21 17:06
     **/
    private static void writerXmlFile(Document doc, File xmlFile) throws TransformerException {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        DOMSource domSource = new DOMSource(doc);
        StreamResult streamResult = new StreamResult(xmlFile);
        transformer.transform(domSource, streamResult);
    }

    /**
     * 创建参数配置文件
     *
     * @param xmlFile          文件绝对路径，该文件需存在
     * @param contentParameter 参数内容
     * @return
     * @author Eugenema
     * @date 2023/10/18 22:13
     **/
    public static boolean createParameterXml(File xmlFile, ObservableList<ParameterMap> contentParameter) {
        // 文件必须存在
        if (!xmlFile.exists()) {
            logger.error("文件【{}】不存在，另存为失败！", xmlFile.getAbsolutePath());
        }
        try {
            /** 文件 key */
            String parameterKey = contentParameter.get(0).parameterKey().get();
            // 创建DocumentBuilderFactory实例
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            // 使用工厂创建DocumentBuilder实例
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            // 创建一个新的Document实例
            Document doc = dBuilder.newDocument();
            // 新增 unit 元素
            Element unit = doc.createElement("unit");
            doc.appendChild(unit);
            // 新增 parameters 元素
            Element parameters = doc.createElement(ELEMENT_PARAMETERS);
            unit.appendChild(parameters);
            // 新增 parameter 元素
            Element parameter = doc.createElement(ELEMENT_PARAMETER);
            parameters.appendChild(parameter);
            parameter.setAttribute(ATTR_KEY, parameterKey);
            // 新增参数项
            for (ParameterMap parameterMap : contentParameter) {
                Element addParameterItem = doc.createElement(ELEMENT_PARAMETER_ITEM);
                addParameterItem.setAttribute(ATTR_KEY, parameterMap.nameProperty().get());
                addParameterItem.setAttribute(ATTR_VALUE, parameterMap.linkProperty().get());
                parameter.appendChild(addParameterItem);
            }
            writerXmlFile(doc, xmlFile);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("未知错误，另存为失败！", e);
            return false;
        }
        return true;
    }

    /**
     * 解析 case 元素
     *
     * @param element 要解析的元素
     * @return 对应的实体类
     * @author 马寅广
     * @date 2023/3/9 9:08
     **/
    private static TestCase parseTestCase(Element element) {
        if (element == null) {
            throw new RuntimeException(ERROR_TIP_PARSE + "case 元素为空");
        }

        TestCase testCase;
        //根据 case 元素的属性，实例化 TestCase，并注入相应字段值
        NamedNodeMap attrs = element.getAttributes();
        testCase = initToAttributes(attrs, new TestCase());

        //解析 step 元素
        NodeList stepNodes = element.getElementsByTagName(ELEMENT_STEP);
        TestStep testStep;
        /** step 元素列表 */
        LinkedHashSet<TestStep> stepSet = new LinkedHashSet<>();
        for (int i = 0; i < stepNodes.getLength(); i++) {
            testStep = parseTestStep((Element) stepNodes.item(i));

            //校验 case 元素 id
            if (!stepSet.add(testStep)) {
                logger.error("{}step 元素 id 属性存在多个【{}】，请检查 id 配置", ERROR_TIP_PARSE, testStep.getId());
                throw new RuntimeException(String.format("%sstep 元素 id 属性存在多个【%s】，请检查 id 配置", ERROR_TIP_PARSE, testStep.getId()));
            }
        }
        testCase.setStepSet(stepSet);
        return testCase;
    }

    /**
     * 解析 step 元素为 TestStep 实例
     *
     * @param element 要解析的元素
     * @return 解析后的实体类。解析失败返回 null
     * @author 马寅广
     * @date 2023/3/10 9:20
     **/
    private static TestStep parseTestStep(Element element) {
        if (element == null) {
            throw new RuntimeException(ERROR_TIP_PARSE + "step 元素为空");
        }

        //初始化对象属性值
        TestStep testStep;
        testStep = initToAttributes(element.getAttributes(), new TestStep());
        return testStep;
    }

    /**
     * 解析公共参数
     * <br/>若 key 值重复，则报错
     *
     * @param element 要解析的 parameter 元素
     * @author Eugenema
     * @date 2023/3/11 22:18
     **/
    private static void parseParameter(Element element) {
        if (element == null) {
            throw new RuntimeException(ERROR_TIP_PARSE + "parameter 元素为空");
        }

        String key = element.getAttribute(ATTR_KEY);
        String value = element.getAttribute(ATTR_VALUE);
        // 由于各个需求的配置分开，故不再做重复 key 校验
//        if (parameterMap.get(key) != null) {
//            throw new RuntimeException(String.format("%sparameter 元素 key 属性值【%s】重复", ERROR_TIP_PARSE, key));
//        }

        //解析 parameter 元素
        if (StringUtils.isBlank(value)) {
            //没有 value，解析 parameterItem 元素
            NodeList parameterItemNodes = element.getElementsByTagName(ELEMENT_PARAMETER_ITEM);

            //仅检查第一个 parameterItem 元素是否有 key
            if (StringUtils.isBlank(((Element) (parameterItemNodes.item(0))).getAttribute(ATTR_KEY))) {
                //parameterItem 没有 key，解析成 List<String>
                ArrayList<String> parameterItemList = new ArrayList<>();
                for (int i = 0; i < parameterItemNodes.getLength(); i++) {
                    Element parameterItem = (Element) (parameterItemNodes.item(i));
                    parameterItemList.add(parameterItem.getAttribute(ATTR_VALUE));
                }
                parameterMap.put(key, parameterItemList);
            } else {
                //parameterItem 有 key，解析成 Map<String, String>
                LinkedHashMap<String, String> parameterItemMap = new LinkedHashMap<>();
                for (int i = 0; i < parameterItemNodes.getLength(); i++) {
                    Element parameterItem = (Element) (parameterItemNodes.item(i));
                    parameterItemMap.put(parameterItem.getAttribute(ATTR_KEY), parameterItem.getAttribute(ATTR_VALUE));
                }
                parameterMap.put(key, parameterItemMap);
            }
        } else {
            //有 value，则认定存储 string
            parameterMap.put(key, value);
        }
    }

    /**
     * 根据 xml 文件中的元素属性，为对象的对应字段注入值
     *
     * @param attrs 元素属性
     * @param t     要注入属性值的对象
     * @return 注入属性值后的对象
     * @author Eugenema
     * @date 2023/3/9 22:10
     **/
    private static <T> T initToAttributes(NamedNodeMap attrs, T t) {
        if (attrs == null || attrs.getLength() == 0) {
            throw new RuntimeException(ERROR_TIP_INIT_ATTR + "属性列表为空");
        }

        //通过反射，逐个注入字段值
        Node attr;
        String name, value;
        for (int i = 0; i < attrs.getLength(); i++) {
            attr = attrs.item(i);
            if (attr == null) {
                continue;
            }

            name = attr.getNodeName();
            value = attr.getNodeValue();
            setByReflect(name, value, t);
        }
        return t;
    }

    /**
     * 通过反射，为对象的对应字段注入值
     *
     * @param name  属性名
     * @param value 属性值
     * @param t     对象
     * @return 赋值后的对象
     * @author Eugenema
     * @date 2023/3/9 22:16
     **/
    private static <T> T setByReflect(String name, String value, T t) {
        if (t == null) {
            throw new RuntimeException(ERROR_TIP_REFLECT + "对象为空");
        }
        if (StringUtils.isBlank(name)) {
            throw new RuntimeException(ERROR_TIP_REFLECT + "属性值为空");
        }

        Class<?> clazz = t.getClass();
        Method setMethod, getMethod;
        try {
            /** 方法后半部分，首字母大写 */
            String methodStr = name.substring(0, 1).toUpperCase() + name.substring(1);

            //拼接方法前半部分
            getMethod = clazz.getMethod(METHOD_PRE_GET + methodStr, new Class<?>[]{});
            setMethod = clazz.getMethod(METHOD_PRE_SET + methodStr, getMethod.getReturnType());

            switch (name) {
                case SPECIAL_ACTION:
                    //根据 StepAction 类中的 map 来获取名称对应的 StepAction（枚举）实例
                    setMethod.invoke(t, StepAction.getAction(value));
                    break;
                case SPECIAL_CANCEL:
                case SPECIAL_NO_HEAD_LESS:
                case SPECIAL_PROFILE:
                    //将字符串转换成 boolean 类型
                    setMethod.invoke(t, BOOLEAN_TRUE.equals(value));
                    break;
                case SPECIAL_LOCATOR:
                    //转换成枚举类
                    setMethod.invoke(t, Locator.valueOf(value));
                    break;
                case SPECIAL_EXCEL_ENTITY:
                    //转换成 class
                    setMethod.invoke(t, Class.forName(value));
                    break;
                case SPECIAL_READ_LISTENER:
                    //转换成监听类
                    setMethod.invoke(t, Class.forName(value).getConstructor().newInstance());
                    break;
                default:
                    //string 类型的属性直接赋值
                    setMethod.invoke(t, value);
            }
        } catch (Exception e) {
            throw new RuntimeException(String.format("%sname【%s】，value【%s】", ERROR_TIP_REFLECT, name, value), e);
        }
        return t;
    }

    /**
     * 解析 EL 表达式
     * <br/>格式：${key}(获取 value)、${key[index]}（获取 list 指定索引的值）
     * <br/>只考虑以下情况：
     * <br/>Map<String, String>
     * <br/>Map<String, List<Object>>
     *
     * @param elStr  要解析的字符串
     * @param tClass 要获取的值的类型
     * @return 解析后的数据
     * @author 马寅广
     * @date 2023/3/14 10:34
     **/
    public static <T> T parseEL(String elStr, Class<T> tClass) throws Exception {
        //校验格式
        if (StringUtils.isBlank(elStr) || !elStr.matches(REGEX_EL_FORMAT)) {
            throw new RuntimeException(String.format("要解析的 EL 表达式【%s】不合法", elStr));
        }

        //去除 ${ 和 }
        elStr = elStr.replaceAll(REGEX_TRIM, "");

        //分割 key 和索引
        String key = null;
        String index = null;
        Pattern pattern = Pattern.compile(REGEX_SPLIT);
        Matcher matcher = pattern.matcher(elStr);
        if (matcher.find()) {
            key = matcher.group(1);
            index = matcher.group(2);
        }

        //通过 key 获取全局存储 map（Selenium.globalMap）中的 value
        Object value = SeleniumUtil.globalMap.get(key);
        if (value == null) {
            throw new RuntimeException(String.format("未找到 key【%s】的 value", key));
        }

        //返回 List 中指定索引的元素
        if (index != null) {
            if (!(value instanceof List)) {
                throw new RuntimeException(String.format("非 List 元素，无法获取索引【】的值", index));
            }
            return (T) ((List) value).get(Integer.valueOf(index));
        }

        //返回从 map 中获取的 value
        return (T) value;
    }

    /**
     * 更新脚本文件
     *
     * @author Eugenema
     * @date 2023/12/6 22:47
     *
     * @param updateContent 更新内容<脚本 id, 值>
     * @param scriptFileName 脚本文件名，包括扩展名
     *
     **/
    public static void updateScript(String scriptFileName, Map<String, String> updateContent) {
        Document document = readXml(scriptFileName);
        Element root = document.getDocumentElement();
        // 获取脚本步骤元素
        Map<String, Element> stepMap = new HashMap<>(10);
        NodeList stepList = root.getElementsByTagName(ELEMENT_STEP);
        for (int i = 0; i < stepList.getLength(); i++) {
            Element step = (Element) stepList.item(i);
            String id = step.getAttribute("id");
            stepMap.put(id, step);
        }

        // 更新脚本
        for (Map.Entry<String, String> updateEntry : updateContent.entrySet()) {
            Element step = stepMap.get(updateEntry.getKey());
            if (step != null) {
                String[] valueArray = updateEntry.getValue().split(SEPARATOR_SCRIPT_UPDATE);
                if (valueArray.length > 1) {
                    step.setAttribute(valueArray[0], valueArray[1]);
                }
            }
        }

        // 保存更改
        try {
            writerXmlFile(document, getTransferFile(scriptFileName));
        } catch (TransformerException e) {
            logger.error("更新脚本文件失败！", e);
        }
    }
}
